Python:Pygame OOP Framework
=Introduction= When you find tutorials on game programming, you tend to find one of two things: The very basics, or specialized topics. Somewhere in the middle is the assumption that you've built on the basics and now just need specific help. Very few places actually have tutorials for this middle ground. This tutorial intends to do just that, using the pygame library for python. While it's language specific in that, the ideas are portable to any language. Requirements The code in this tutorial has been tested on two of my machines - a linux machine and an iBook - so I'm reasonably sure of its portability. You'll need the following: * python version 2.3 or later * pygame version 1.6 with TTF support built in A Word About Style The framework will use a number of python files, but they'll be large and we don't want to show them all at once. So we'll just be showing parts as we go along, and then the entire code dump at the end. Portions of code will look like this: example.py if __name__ '__main__': print "Hello, world!" And later, when we add code to that: example.py def hiWorld(): print "Hello, world!" Sometimes, a method or class will be too freaking huge for me to put in one place. Python's pretty sensitive to indentation, so I'll try not to do this, but at times it makes for far more clear code. In theory, you should be able to just stitch the code together. example.py (reallyLongFunction, part 1) def reallyLongFunction(): print "This is a really really", example.py (reallyLongFunction, part 2) print "really really REALLY long function" Finally, the code dump: example.py (entire) def reallyLongFunction(): print "This is a really really", print "really really REALLY long function" def hiWorld(): print "Hello, world!" if __name__ '__main__': hiWorld() =The Very Basics= Because I've done too much Smalltalk programming, I tend to over-object-orient things. One of the nice things Smalltalk had was a built-in 'SubclassShouldImplement' function, which told you that you'd called an abstract method. I re-create that for python here: SubclassShouldImplement common.py class SubclassShouldImplement(Exception): def __init__(self, msg="A method was called which should have been overridden"): Exception.__init__(self,msg) This allows us to have classes which are explicitly abstract. There's also the nice side-effect that pychecker assumes any class method which does nothing but raise an exception is abstract, and so it'll warn us if we accidentally call it. Updateable Every game has objects that need to be updated on a regular basis. This very simple base class is for them: gui.py from pygame.constants import * from common import SubclassShouldImplement class Updateable: def update(self,delay): "delay is the time in seconds passed since last iteration" raise SubclassShouldImplement Paintable Now for things which go on the screen: gui.py class Paintable: def __init__(self, loc=None): """loc is a tuple of the upper-left location to paint this paintable at. Subclasses (such as Mouseable) depend on the first two entries being x,y""" self.loc = loc def paint(self,screen): raise SubclassShouldImplement This is to be the parent class of everything we put on the screen. All it needs to do is know where it is and how to paint itself. Mouseable Of course, being able to paint things is handy, but being able to actually move them and interact would be nicer. Thus, we'll make another abstract class. This will inherit from Paintable, as in order to click on something it has to be able to show up on the screen to begin with. gui.py class Mouseable(Paintable): def __init__(self,bounds = None): """bounds is the location and width/height of the mouseable. If None, we're everywhere!""" Paintable.__init__(self,bounds) self.buttonState = MOUSEBUTTONUP def mouseEvent(self,event): "event is a MOUSE* event, this routine decodes it and calls one of the subs" x,y = event.pos if event.type MOUSEBUTTONDOWN: self.buttonState = event.type self.mouseDownEvent(x,y) elif event.type MOUSEBUTTONUP: self.buttonState = event.type self.mouseUpEvent(x,y) elif event.type MOUSEMOTION: if self.buttonState MOUSEBUTTONDOWN: self.mouseDragEvent(x,y) self.mouseMoveEvent(x,y) def mouseDownEvent(self,x,y): pass def mouseUpEvent(self,x,y): pass def mouseDragEvent(self,x,y): pass def mouseMoveEvent(self,x,y): pass The most important thing that this class does is to take raw pygame events and translate them into function calls. Subclasses only need to override any of the mouse*Event functions. Note that this doesn't actually make sure the mouse event took place within the bounds of the Mouseable object. Our engine will do this before sending the event, but it would possibly be more object-oriented for us to do so here. Keyable Finally, we need a class for those objects which will listen to us hitting buttons gui.py class Keyable: def __init__(self, keys=None): """keys is a list of keys that this will respond to. If None, it listens to everything""" self.keys = keys def maskEvent(self, key, unicode, pressed): if self.keys: if not (key in self.keys): return self.keyEvent(key,unicode,pressed) def keyEvent(self,key,unicode, pressed): raise SubclassShouldImplement Here, a Keyable registers a list of keys that it will respond to. Our system calls maskEvent, which then calls keyEvent. =The State Machine= Typically, a game has a number of different modes. A game of pong, for instance, has a title screen, the actual playing of the game, and a game over screen. What you'd normally do is have a variable indicating what state you're on, and branch on that. Of course, the whole reason I'm writing the tutorial is to avoid that sort of thing. State The current state of the game can be represented by an object. Each state, after all, has a certain number of things in common: states.py import pygame import sys import gui # So we can have a common interface between gui stuff and state stuff from common import SubclassShouldImplement from pygame.locals import * class State(gui.Keyable,gui.Mouseable): def __init__(self, driver,screen): gui.Keyable.__init__(self) # States listen to everything gui.Mouseable.__init__(self) self._driver = driver self.screen = screen def activate(self): pass # maskEvent is handled by Keyable def keyEvent(self,key,unicode,pressed): pass def paint(self,screen): raise SubclassShouldImplement def reactivate(self): pass def update(self, delay): pass These functions are the basis of everything you'd want to do in a state. keyEvent and paint you're already familiar with. activate is called when the state is first made active, reactivate when another state's This is, of course, just the bare bones of what a state should be. That's the whole idea behind making it object oriented, after all. StateDriver Most of the work in the game will be done by the states, but on their own they don't really do much. There's need for glue to put them together. Here's the basic outline of the StateDriver class: states.py (StateDriver, part 1) class StateDriver: def __init__(self, screen): self._states = [] self._screen = screen def done(self): self._states.pop() self.getCurrentState().reactivate() def getCurrentState(self): try: return self._states- 1 except IndexError: raise SystemExit # we're done if theren't any states left def getScreenSize(self): return self._screen.get_size() def quit(self): # Was 'raise SystemExit', but pychecker assumes any function that # unconditionally raises an exception is abstract sys.exit(0) def replace(self, state): self._states.pop() self.start(state) The StateDriver acts like a stack. While this isn't important most of the time, it's most useful for when you need to implement something like a 'paused' state. Simply push the pause state on the stack when the game needs to stop, and pop it when you're done. States respond to events, as you've seen above, but who sends the events out? This also seems like the perview of the StateDriver: states.py (StateDriver, part 2) def run(self): currentState = self.getCurrentState() lastRan = pygame.time.get_ticks() while(currentState): # poll queue event = pygame.event.poll() while(event.type <> NOEVENT): if event.type QUIT: currentState = None break elif event.type KEYUP or event.type KEYDOWN: if event.key K_ESCAPE: currentState = None break if event.type KEYUP: currentState.maskEvent(event.key, None, 0) if event.type KEYDOWN: currentState.maskEvent(event.key, event.unicode, 1) elif (event.type MOUSEMOTION): currentState.mouseEvent(event) elif (event.type MOUSEBUTTONDOWN or event.type MOUSEBUTTONUP): currentState.mouseEvent(event) event = pygame.event.poll() self._screen.fill( (0, 0, 0) ) if currentState: currentState.paint(self._screen) curTime = pygame.time.get_ticks() elapsed = float(curTime-lastRan)/1000.0 currentState.update(elapsed) lastRan = curTime currentState = self.getCurrentState() pygame.display.flip() pygame.time.delay(40); def start(self, state): self._states.append(state) self.getCurrentState().activate() There are a few design decisions here. For instance, if the player hits the escape key, the program ends right then and there. Secondly, the driver waits for 40 milliseconds each frame. This is primarily so the rest of the system doesn't get bogged down. Unfortunately, this locks the game down to a fixed FPS at best. For now, it'll do, but in the future you might want to replace it. Thirdly, it clears the screen each time. We could possibly save time by not doing that, and keeping track of what's changed and just re-painting that. Clearing the whole screen is simpler, though. GuiState While we've got a basic state, we don't have one that will actually do anything. We went through all the trouble of making states and paintables and everything, we should do something with them! states.py class GuiState(State): def __init__(self,driver, screen): State.__init__(self,driver,screen) self.paintables = [] self.mouseables = [] self.keyables = [] self.updateables = [] def add(self,item): # Add to the appropriate list(s) based on type if(isinstance(item,gui.Paintable)): self.paintables.append(item) if(isinstance(item,gui.Mouseable)): self.mouseables.append(item) if(isinstance(item,gui.Keyable)): self.keyables.append(item) if(isinstance(item,gui.Updateable)): self.updateables.append(item) def paint(self,screen): for paintable in self.paintables: paintable.paint(screen) def keyEvent(self,key,unicode,pressed): for keyable in self.keyables: keyable.keyEvent(key,unicode,pressed) def mouseEvent(self,event): x,y = event.pos for mouseable in self.mouseables: x1,y1 = mouseable.loc0:2 try: w,h = mouseable.loc2:4 except IndexError: w,h = self.screen.get_width(),self.screen.get_height() if ( x >= x1 and y >= y1 and x < x1+w and y < y1+h): mouseable.mouseEvent(event) def update(self,delay): for updateable in self.updateables: updateable.update(delay) =Moving On= You've got a framework, but what can you do with it? While it's (hopefully) easy enough to take the code here and create something with it, tutorials are designed to teach - thus I've made a follow-up on using the framework to make Pong =Code Dump, Part I= common.py class SubclassShouldImplement(Exception): def __init__(self, msg="A method was called which should have been overridden"): Exception.__init__(self,msg) gui.py from common import SubclassShouldImplement from pygame.constants import * class Paintable: """loc is a tuple of the upper-left location to paint this paintable at. Subclasses (such as Mouseable) depend on the first two entries being x,y""" def __init__(self, loc=None): self.loc = loc def paint(self,screen): raise SubclassShouldImplement class Mouseable(Paintable): """bounds is the location and width/height of the mouseable. If None, we're everywhere!""" def __init__(self,bounds = None): Paintable.__init__(self,bounds) self.buttonState = MOUSEBUTTONUP def mouseEvent(self,event): "event is a MOUSE* event, this routine decodes it and calls one of the subs" x,y = event.pos if event.type MOUSEBUTTONDOWN: self.buttonState = event.type self.mouseDownEvent(x,y) elif event.type MOUSEBUTTONUP: self.buttonState = event.type self.mouseUpEvent(x,y) elif event.type MOUSEMOTION: if self.buttonState MOUSEBUTTONDOWN: self.mouseDragEvent(x,y) self.mouseMoveEvent(x,y) def mouseDownEvent(self,x,y): pass def mouseUpEvent(self,x,y): pass def mouseDragEvent(self,x,y): pass def mouseMoveEvent(self,x,y): pass class Keyable: def __init__(self, keys=None): """keys is a list of keys that this will respond to. If None, it listens to everything""" self.keys = keys def maskEvent(self, key, unicode, pressed): if self.keys: if not (key in self.keys): return self.keyEvent(key,unicode,pressed) def keyEvent(self,key,unicode, pressed): raise SubclassShouldImplement class Updateable: def update(self,delay): "delay is the time in seconds passed since last iteration" raise SubclassShouldImplement states.py import pygame import sys import gui # So we can have a common interface between gui stuff and state stuff from common import SubclassShouldImplement from pygame.locals import * class StateDriver: def __init__(self,screen): self._states = [] self._screen = screen def done(self): self._states.pop() self.getCurrentState().reactivate() def getCurrentState(self): try: return self._states- 1 except IndexError: raise SystemExit # we're done if theren't any states left def getScreenSize(self): return self._screen.get_size() def quit(self): # Was 'raise SystemExit', but pychecker assumes any function that # unconditionally raises an exception is abstract sys.exit(0) def replace(self, state): self._states.pop() self.start(state) def run(self): currentState = self.getCurrentState() lastRan = pygame.time.get_ticks() while(currentState): # poll queue event = pygame.event.poll() while(event.type != NOEVENT): if event.type QUIT: currentState = None break elif event.type KEYUP or event.type KEYDOWN: if event.key K_ESCAPE: #currentState = None #break pass if event.type KEYUP: currentState.maskEvent(event.key, None, 0) if event.type KEYDOWN: currentState.maskEvent(event.key, event.unicode, 1) elif (event.type MOUSEMOTION): currentState.mouseEvent(event) elif (event.type MOUSEBUTTONDOWN or event.type MOUSEBUTTONUP): currentState.mouseEvent(event) event = pygame.event.poll() self._screen.fill( (0, 0, 0) ) if currentState: currentState.paint(self._screen) curTime = pygame.time.get_ticks() elapsed = float(curTime-lastRan)/1000.0 currentState.update(elapsed) lastRan = curTime currentState = self.getCurrentState() pygame.display.flip() pygame.time.delay(40); def start(self, state): self._states.append(state) self.getCurrentState().activate() class State(gui.Keyable,gui.Mouseable): def __init__(self, driver,screen): gui.Keyable.__init__(self) # States listen to everything gui.Mouseable.__init__(self) self._driver = driver self.screen = screen def activate(self): pass # maskEvent is handled by Keyable def keyEvent(self,key,unicode,pressed): pass def paint(self,screen): raise SubclassShouldImplement def reactivate(self): pass def update(self, delay): pass class GuiState(State): def __init__(self,driver, screen): State.__init__(self,driver,screen) self.paintables = [] self.mouseables = [] self.keyables = [] self.updateables = [] def add(self,item): # Add to the appropriate list(s) based on type if(isinstance(item,gui.Paintable)): self.paintables.append(item) if(isinstance(item,gui.Mouseable)): self.mouseables.append(item) if(isinstance(item,gui.Keyable)): self.keyables.append(item) if(isinstance(item,gui.Updateable)): self.updateables.append(item) def paint(self,screen): for paintable in self.paintables: paintable.paint(screen) def keyEvent(self,key,unicode,pressed): for keyable in self.keyables: keyable.keyEvent(key,unicode,pressed) def mouseEvent(self,event): x,y = event.pos for mouseable in self.mouseables: x1,y1 = mouseable.loc0:2 try: w,h = mouseable.loc2:4 except IndexError: w,h = self.screen.get_width(),self.screen.get_height() if ( x >= x1 and y >= y1 and x < x1+w and y < y1+h): mouseable.mouseEvent(event) def update(self,delay): for updateable in self.updateables: updateable.update(delay) category:Python category:Pygame category:Object-Oriented Programming category:Tutorial